package jmine.tec.script.persistent.domain;

import java.util.Collection;
import java.util.HashSet;
import java.util.Set;

import javax.persistence.Column;
import javax.persistence.DiscriminatorColumn;
import javax.persistence.DiscriminatorType;
import javax.persistence.DiscriminatorValue;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
import javax.persistence.ManyToOne;
import javax.persistence.SequenceGenerator;
import javax.persistence.Table;
import javax.persistence.Transient;

import jmine.tec.persist.annotation.Alias;
import jmine.tec.persist.annotation.Comment;
import jmine.tec.persist.annotation.Comments;
import jmine.tec.persist.annotation.Constraint;
import jmine.tec.persist.annotation.DiscriminatorComment;
import jmine.tec.persist.annotation.Index;
import jmine.tec.persist.annotation.UniqueConstraint;
import jmine.tec.script.ScriptDefinition;
import jmine.tec.script.ScriptExecutionContext;
import jmine.tec.script.persistent.ScriptController;

import org.hibernate.annotations.Cascade;
import org.hibernate.annotations.CascadeType;
import org.hibernate.annotations.CollectionOfElements;
import org.hibernate.annotations.ForeignKey;
import org.hibernate.annotations.Type;
import org.hibernate.validator.NotNull;

import bancosys.tec.component.Documentation;
import bancosys.tec.persist.bussobj.PersistableBusinessObject;
import bancosys.tec.persist.persister.annotation.NaturalKey;
import bancosys.tec.persist.persister.annotation.SystemId;
import bancosys.tec.utils.md5sum.MD5SumTool;

/**
 * @author piercio
 */
@Entity
@Table(name = "SCRIPT")
@DiscriminatorColumn(name = "TP_SCRIPT", discriminatorType = DiscriminatorType.INTEGER)
@DiscriminatorValue("0")
@DiscriminatorComment("SCRIPT PADRAO")
@Alias("SCRIPT")
@SequenceGenerator(name = "SEQ_SCRIPT", sequenceName = "SEQ_SCRIPT")
@Documentation("ARMAZENA OS SCRIPTS.")
@Comment(table = "SCRIPT", column = "TP_SCRIPT", value = "TIPO DE SCRIPT.")
@Constraint(suffix = "0")
public class Script extends PersistableBusinessObject implements ScriptDefinition {

    private static final int HEXA = 16;

    private Long id;

    private long version;

    private Integer discriminator;

    private String mnemonico;

    private String descricao;

    private String body;

    private ScriptPurpose purpose;

    private ScriptLanguage language;

    private Set<Script> apis = new HashSet<Script>();

    private Set<ScriptDependency> dependencies = new HashSet<ScriptDependency>();

    /**
     * Cria o contexto de execução do script.
     * 
     * @return context
     */
    public ScriptExecutionContext<?> createContext() {
        return this.getScriptController().getContextFactory().createContext(this);
    }

    /**
     * Cria o contexto de execução de script, que deve subtipo da classe dada.
     * 
     * @param <C> superclasse ou interface do contexto.
     * @param ctxInterface superclasse ou interface do contexto.
     * @return contexto
     */
    public <C> C createContext(Class<C> ctxInterface) {
        return this.getScriptController().getContextFactory().createContext(ctxInterface, this);
    }

    /**
     * Cria o contexto de obtenção de dependências do script.
     * 
     * @return context
     */
    public ScriptExecutionContext<?> createDependencyContext() {
        return this.getScriptController().getContextFactory().createDependencyContext(this);
    }

    /**
     * Cria o contexto de obtenção de dependências de script, que deve subtipo da classe dada.
     * 
     * @param <C> superclasse ou interface do contexto.
     * @param ctxInterface superclasse ou interface do contexto.
     * @return contexto
     */
    public <C> C createDependencyContext(Class<C> ctxInterface) {
        return this.getScriptController().getContextFactory().createDependencyContext(ctxInterface, this);
    }

    /**
     * Cria o contexto de validação do script.
     * 
     * @return context
     */
    public ScriptExecutionContext<?> createValidationContext() {
        return this.getScriptController().getContextFactory().createValidationContext(this);
    }

    /**
     * Cria o contexto de validação de script, que deve subtipo da classe dada.
     * 
     * @param <C> superclasse ou interface do contexto.
     * @param ctxInterface superclasse ou interface do contexto.
     * @return contexto
     */
    public <C> C createValidationContext(Class<C> ctxInterface) {
        return this.getScriptController().getContextFactory().createValidationContext(ctxInterface, this);
    }

    /**
     * @param body o valor para o corpo do script
     */
    public void setBody(String body) {
        this.body = body;
    }

    /**
     * @return o id do script.
     */
    @Id
    @SystemId
    @GeneratedValue(strategy = GenerationType.AUTO, generator = "SEQ_SCRIPT")
    @Column(name = "COD_SCRIPT", nullable = false)
    @Documentation("CODIGO DO SCRIPT.")
    public Long getId() {
        return this.id;
    }

    /**
     * @return the version
     */
    @Documentation("VERSAO DO SCRIPT.")
    @Column(name = "VERSION", nullable = false)
    public long getVersion() {
        if (this.version == 0) {
            recalculateVersion();
        }
        return this.version;
    }

    /**
     * Recalculates the version
     */
    public void recalculateVersion() {
        long oldVersion = this.version;
        this.version = calculateScriptVersion(this);
        if (this.version != 0 && this.version == oldVersion) {
            this.version++;
        }
    }

    /**
     * Calculates a long to be the version of the script
     * 
     * @param script {@link Script}
     * @return long
     */
    private static long calculateScriptVersion(Script script) {
        long version = 0;
        if (script.getBody() != null) {
            String md5Sum = MD5SumTool.md5Sum(script.getBody());
            version = toLong(md5Sum);
        }
        return version;
    }

    /**
     * Converts a String with an md5sum into a long (shifts and substrings)
     * 
     * @param md5Sum String
     * @return long
     */
    private static long toLong(String md5Sum) {
        final int segments = 4;
        long result = 0;
        final int size = HEXA / 2;
        String md5 = md5Sum;
        for(int i = 0; i < segments; i++) {
            result ^= Long.parseLong(md5.substring(0, size), HEXA);
            md5 = md5.substring(size);
        }
        return result;
    }

    /**
     * @return the discriminator
     */
    @SuppressWarnings("unused")
    @Column(name = "TP_SCRIPT", insertable = false, updatable = false)
    private Integer getDiscriminator() {
        return this.discriminator;
    }

    /**
     * @return the mnemonico
     */
    @NaturalKey
    @UniqueConstraint(name = "AK_SCRIPT_0")
    @NotNull
    @Column(name = "MNE", nullable = false)
    @Documentation("MNEMONICO DO SCRIPT.")
    public String getMnemonico() {
        return this.mnemonico;
    }

    /**
     * @return the descricao
     */
    @Column(name = "DESCRICAO")
    @Documentation("DESCRICAO DO SCRIPT")
    public String getDescricao() {
        return this.descricao;
    }

    /**
     * @return bodyClob the bodyClob to set
     */
    @Type(type = "org.hibernate.type.TextType")
    @Column(name = "BODY", length = 10485760)
    @Documentation("CORPO DO SCRIPT.")
    public String getBody() {
        return this.body;
    }

    /**
     * @return the purpose
     */
    @Index(suffix = "JT_1")
    @NotNull
    @ManyToOne
    @JoinColumn(name = "COD_SCRIPT_PURPOSE", nullable = false)
    @Documentation("CODIGO DO PROPOSITO DO SCRIPT.")
    @Cascade(CascadeType.LOCK)
    public ScriptPurpose getPurpose() {
        return this.purpose;
    }

    /**
     * @return the language
     */
    @Index(suffix = "JT_0")
    @NotNull
    @ManyToOne
    @JoinColumn(name = "COD_SCRIPT_LANGUAGE", nullable = false)
    @Documentation("CODIGO DA LINGUAGEM DO SCRIPT.")
    public ScriptLanguage getLanguage() {
        return this.language;
    }

    /**
     * @return the apis
     */
    @ManyToMany
    @JoinTable(name = "SCRIPT_API", joinColumns = @JoinColumn(name = "COD_SCRIPT", nullable = false), inverseJoinColumns = @JoinColumn(name = "COD_SCRIPT_API", nullable = false))
    @Alias("SCRAPI")
    @Comments({ @Comment(table = "SCRIPT_API", value = "TABELA DE ASSOCIACAO ENTRE UM SCRIPT E AS SUAS APIS."),
            @Comment(table = "SCRIPT_API", column = "COD_SCRIPT", value = "CODIGO DO SCRIPT BASE."),
            @Comment(table = "SCRIPT_API", column = "COD_SCRIPT_API", value = "CODIGO DO SCRIPT QUE E API DO SCRIPT BASE.") })
    @Cascade(CascadeType.LOCK)
    @ForeignKey(name = "FK_SCRIPT_SCRAPI_1")
    public Set<Script> getApis() {
        return this.apis;
    }

    /**
     * @return the dependencies
     */
    @Index(suffix = "JT_0", columns = "COD_SCRIPT")
    @CollectionOfElements
    @Cascade({ CascadeType.ALL, CascadeType.DELETE_ORPHAN })
    @JoinTable(name = "SCRIPT_DEPENDENCY", joinColumns = @JoinColumn(name = "COD_SCRIPT", nullable = false))
    @Alias("SCRDEP")
    @Comments({ @Comment(table = "SCRIPT_DEPENDENCY", value = "ARMAZENA DEPENDENCIAS DE INJECAO DOS SCRIPTS."),
            @Comment(table = "SCRIPT_DEPENDENCY", column = "BEAN", value = "NOME DO BEAN A SER INJETADO NA BEANFACTORY"),
            @Comment(table = "SCRIPT_DEPENDENCY", column = "VARIABLE", value = "NOME DA VARIAVEL DO SCRIPT QUE DEVE RECEBER O BEAN"),
            @Comment(table = "SCRIPT_DEPENDENCY", column = "COD_SCRIPT", value = "CÓDIGO DAS DEPENDÊNCIAS DE INJEÇÃO DOS SCRIPTS") })
    public Set<ScriptDependency> getDependencies() {
        return this.dependencies;
    }

    /**
     * @param id o valdor do id do script.
     */
    public void setId(Long id) {
        this.id = id;
    }

    /**
     * @param version the version to set
     */
    public void setVersion(long version) {
        this.version = version;
    }

    /**
     * @param discriminator the discriminator to set
     */
    @SuppressWarnings("unused")
    private void setDiscriminator(Integer discriminator) {
        this.discriminator = discriminator;
    }

    /**
     * @param mnemonico the mnemonico to set
     */
    public void setMnemonico(String mnemonico) {
        this.mnemonico = mnemonico;
    }

    /**
     * @param descricao the descricao to set
     */
    public void setDescricao(String descricao) {
        this.descricao = descricao;
    }

    /**
     * @param purpose the purpose to set
     */
    public void setPurpose(ScriptPurpose purpose) {
        this.purpose = purpose;
    }

    /**
     * @param language the language to set
     */
    public void setLanguage(ScriptLanguage language) {
        this.language = language;
    }

    /**
     * @param apis the apis to set
     */
    protected void setApis(Set<Script> apis) {
        this.apis = apis;
    }

    /**
     * @param dependencies the dependencies to set
     */
    protected void setDependencies(Set<ScriptDependency> dependencies) {
        this.dependencies = dependencies;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String toString() {
        return this.mnemonico + " : " + this.body;
    }

    /**
     * {@inheritDoc}
     */
    @Transient
    public ScriptController getScriptController() {
        return (ScriptController) this.getController();
    }

    /**
     * @return init script entity
     */
    @Transient
    public Script getInitScript() {
        if (this.getPurpose() == null) {
            return null;
        }
        return this.getPurpose().getInitScript(this.getLanguage());
    }

    /**
     * {@inheritDoc}
     */
    public String name() {
        return this.getMnemonico();
    }

    /**
     * {@inheritDoc}
     */
    public String scriptBody() {
        return this.getBody();
    }

    /**
     * {@inheritDoc}
     */
    public Collection<? extends ScriptDefinition> traits() {
        Set<ScriptDefinition> set = new HashSet<ScriptDefinition>();
        for (Script sc : this.getApis()) {
            set.add(sc);
            set.addAll(sc.getApis());
        }
        return set;
    }

    /**
     * {@inheritDoc}
     */
    public Object identifier() {
        return getId();
    }

    /**
     * {@inheritDoc}
     */
    public Object version() {
        return getVersion();
    }
}
